目標
我的最愛功能:
讓使用者可以透過選單的方式,切換到「我的最愛」列表
並且我們將Supabase的資料過濾LocalStorage的資料
顯示在上面;
這個組件還會再分一篇講
這篇主要講的是存取與過濾資料
內容大多數之前的鐵人文章都有講過
請輕鬆閱讀即可。
步驟
1.
先看當初設計的線稿,F組件(我的最愛)是設計在底下
在選單中按下我的最愛時,會出現F組件
另外隱藏其他不必要的組件
所以在E組件的script 會先設計以下架構
<script setup>
/* global defineEmits */
import { ref } from "vue";
const emit = defineEmits(["response"]);
//..中間略
function displayEvent(name){
const displayState = ({
A_Display:true, //敵人資訊
B_Display:true,//活動資訊
C_Display:true,//副本資訊
D_Display:false,//條件查詢
E_Display:true,//選單
F_Display:false//我的最愛
})
if(name == "活動一覽") {
displayState.B_Display = true;
displayState.C_Display = true;
displayState.D_Display = false;
displayState.F_Display = false;
}
if(name == "我的最愛") {
displayState.B_Display = true;
displayState.F_Display = true;
displayState.C_Display = false;
displayState.D_Display = false;
}
if(name == "條件查詢") {
displayState.D_Display = true;
displayState.B_Display = false;
displayState.C_Display = false;
displayState.F_Display = false;
}
emit("response", displayState);
}
</script>
然後在template補上觸發事件
在分別點擊活動一覽、我的最愛、條件查詢時
會分別將特定屬性做切換
而這個東西 就靠之前說的emit傳遞給父組件
<div class="d-grid gap-2">
<button type="button" class="btn btn-primary" v-for="item in menuList" :key="item.id"
@click="displayEvent(item.name)">
{{item.name}}
</button>
</div>
要在父組件(APP.vue)這邊接收E組件傳過來的變化前
首先要先在父組件的script做一個初始設定
<script setup>
//..以上略
const displayState = ref({
A_Display:true,
B_Display:true,
C_Display:true,
D_Display:false,
E_Display:true,
F_Display:false
});
</script>
然後在template上做判斷
<div class="row" >
<div class = "col-6 " style="height:550px;">
<A-enemy :msg="C_Msg" v-show="displayState.A_Display" />
</div>
<div class = "col-5 ">
<div class="row" v-show="displayState.B_Display">
<div class = "col-12 border" style="height:250px;overflow:auto;" >
<B-action @response="(msg) => B_Msg = msg" />
</div>
</div>
<div class="row" v-show="displayState.C_Display">
<div class = "col-12 border" style="height:300px;overflow:auto;">
<C-event :msg="B_Msg" @response="(msg) => C_Msg = msg" />
</div>
</div>
<div class="row" v-show="displayState.D_Display">
<div class = "col-12 border" style="height:550px">
<D-skillSearch />
</div>
</div>
<div class="row" v-show="displayState.F_Display">
<div class = "col-12 border" style="height:300px">
<F-favoriteList />
</div>
</div>
</div>
<div class="col-1 border" v-show="displayState.E_Display" >
<E-menu @response="(msg) => displayState = msg"/>
</div>
</div>
與之前有差異的其實是在紅框這邊
將v-show
加入判斷
當E組件(選單)點擊時,得以判斷哪些組件要顯示或隱藏
由於localStorage已經有了「我的最愛」資料
現在F組件還缺少從Supabase拿過來的資料
因為取資料這件事情已經在B組件做過了
所以這次一樣透過emit和props實現
在B組件中,會像下圖紅框這邊多傳一個emit給父組件
在一開始取Supabase的資料時 也同時傳一份過去
<script setup>
//...以上略
onMounted(async () => {
const { data, error } = await supabase
.from("action_event")
.select("*")
if (error) {
console.error(error);
} else {
result.value = data;
emit("responseBF", result.value);
}
});
</script>
接下來在F組件的script為以下程式結構
裡面很多東西都是抄C組件裡的
/* global defineProps */
import { ref,onMounted,computed,watch } from "vue";
const images = require.context('@/assets/Event', false, /\.png$/)
const favoriteList = ref([]);
const props = defineProps({
msg: Object
})
//若LocalStorage無資料 就新建
onMounted(() => {
if (localStorage.getItem("favoriteList") == null) {
console.log("建立最愛列表");
localStorage.setItem("favoriteList", [])
}
else{
favoriteList.value = JSON.parse(localStorage.getItem("favoriteList"))
}
})
const favoriteFilterList = computed(() => {
if (!Array.isArray(props.msg) || props.msg.length === 0) {
return [];
}
const result = Object.values(
props.msg.filter(x => favoriteList.value.some( y => x.actionNo + x.eventNo + x.stagePrimaryNo + x.stageSecNo + x.stageOrder == y.rrn))
.reduce((acc, cur) => {
const key = cur.eventNo;
if (!acc[key]) {
acc[key] = {
eventNo: cur.eventNo,
eventName: cur.eventName,
eventInfo: []
};
}
const info = cur.stagePrimaryNo + cur.stageSecNo;
if (!acc[key].eventInfo.includes(info)) {
acc[key].eventInfo.push(info);
}
return acc;
}, {})
);
return result;
})
function getImg(eventNo) {
return images(`./E${eventNo}.png`)
}
</script>
template也幾乎照搬再做微調
<template>
我的最愛{{favoriteFilterList}}
<div class="row mb-5" v-for="item in eventList" :key="item.eventNo">
<div class="col-9">
<img :src="getImg(item.eventNo)" style="width:100%;">
</div>
<div class="col-3">
<div class="card" >
<div class="card-header">
{{item.eventName}}
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item" v-for="child in item.eventInfo" :key="child"
:data-eventNo="item.eventNo" :data-stagePrimaryNo="child.substring(0,2)"
:data-stageSecNo="child.substring(2,4)" @click="selectEvent"
>
{{ String(parseInt(child.substring(0,2))).padStart(2, ' ')}}
-
{{ String(parseInt(child.substring(2,4))).padStart(2, ' ')}}
</li>
</ul>
</div>
</div>
</div>
</template>
我們先來看一下線稿時做的規劃
再來回到畫面
當加入我的最愛後,F組件這邊能看到結果
而這篇我們做的就是將B組件傳過來的資料過濾
顯示在F組件 如下圖
但是,此時就會發生一個很嚴重的問題
當A組件加入到我的最愛後A組件存LocalStorage,F組件並沒有同步到
只有在重新整理或透過手動操作,F組件才會重新抓localStorage
我們預期的結果應該是A組件存LocalStorage,F組件也會同步到
但是實際發生的卻是下圖的情況
A組件存、讀取的localStorage實例對象 和F組件不是同一個
我們將在下一篇處理這件事,將F組件的篇章作個了結
備註
1.v-show
與 v-if
是有差異的
在官方文件中有提到
當 v-if 元素被觸發,元素及其包含的指令/組件都被銷毀和重構。
也就是說 如果這邊改用v-if後
因為A組件載入時,會從Supabase拿資料
此時,我們切換選單把B組件隱藏然後又顯示
B組件會再次載入,所以會再次從Supabase拿資料
由於從Supabase拿資料的動作我們是在onMounted做的
所以可以做一個簡單的驗證
把父組件載入B組件的 v-show
換成 v-if
接著我們在B組件的onMounted加入這段
在每次切換選單關掉B組件又打開,便會重新載入
關於最後一個段落的localStorage資料同步問題
也許你可能有疑問,類似下面這樣寫
將localStorage的資料加進實例 是不是就可以了呢?
const storage = ref(JSON.parse(localStorage.getItem("favoriteList")));
watch(storage, (newVal) => { alert("有變更") },{ deep: true });
因為Vue只會監聽響應式物件(如ref)
但localStorage是瀏覽器API,不是Vue的響應式物件
Vue不會去看localStorage的變化
假設接著手動更改 storage.value = []
這種操作是沒有用的